/* eslint-disable @typescript-eslint/naming-convention */

import { Points, BufferGeometry, BufferAttribute,
  WebGLRenderer, HalfFloatType, ClampToEdgeWrapping,
  NearestFilter, Scene, Camera, Group, ShaderMaterial,
  UniformsLib, UniformsUtils, Texture, Color, Vector2, Mesh, Blending, AdditiveBlending, CustomBlending, MaxEquation, MinEquation, ReverseSubtractEquation, SubtractEquation, AddEquation, Vector3, Uniform, Vector4, DataTexture, Matrix4, Material, Matrix3, Object3D, Line } from 'three';
import { GPUComputationRenderer, Variable } from 'three/examples/jsm/misc/GPUComputationRenderer.js';


interface ParticleParams{
  /* 发射源 */
  emit_object?: Line|Mesh|Points;
  /* 发射偏移随机性 */
  emit_random?: number;
  /* 总粒子数 默认: 16 */
  count?: number;
  /* 生命周期 默认: 1 */
  lifetime?: number;
  /* 生命周期次数 默认: -1(永久) */
  lifetime_count?: number;
  /* 生命周期随机性 */
  lifetime_random?: number;
  /* 质量 默认: 1 */
  mass?: number;
  /* 粒子大小 默认: 1 */
  size?: number;
  /* 缩放随机性 */
  size_random?: number;
  /* 实例物体 */
  instance_object?: Line|Mesh|Points;
  /* 速度 */
  velocity?: {
    /* 法向 默认: 1 */
    normal_factor?: number;
    /* 切向 */
    tangent_factor?: number;
    /* 物体速度 */
    object_factor?: Vector3;
    /* 随机 */
    factor_random?: number;
  }
  /* 旋转 */
  rotation?: {
    /* 随机朝向 */
    factor_random?: number;
    /* 角速度 */
    velocity_factor?: number;
  }
  /* 力场 */
  forces?: {
    /* 布朗 */
    brownian?: number;
    /* 阻尼 */
    damping?: number;
    /* 全部 */
    all?: number;
    /* 常力 */
    force?: number;
    /* 风力 */
    wind?: number;
    /* 重力 */
    gravity?: number;
    /* 涡流 */
    vortex?: number;
    /* 纹理 */
    texture?: number;
    /* 紊流 */
    turbulence?: number;
  }
}
/* Particle geometry */
class ParticleGeometry extends BufferGeometry {
  private static gpuInfoShader=`
  // x: totalTime y:deltaTime z:iterationCount
  uniform vec3 time;
  void main()	{
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    // x:index y:random z:age w:lifetime
    gl_FragColor = texture2D( textureInfo, uv );
    if(time.x == 0.){
      gl_FragColor.z = 0.;
      if(gl_FragColor.w <= 0. || time.z == -1.){
        gl_FragColor.w = -.1 - gl_FragColor.y * particleInfo.lifetime * particleInfo.lifetime_random;
      }
      if(time.z == 0. && gl_FragColor.w < 0.){
        gl_FragColor.w = 0.;
      }
    }
    bool isUpdate = time.z < 0. || time.z > gl_FragColor.z;
    if(particleInfo.lifetime > 0. && gl_FragColor.w >= particleInfo.lifetime){ 
      gl_FragColor.z += 1.;
      gl_FragColor.w -= particleInfo.lifetime;
      if(isUpdate){
        float random = (sin(gl_FragColor.x*123. + gl_FragColor.y * 234. + time.y * 345. + 456.)+1.)/2.;
        float randomFrame = random * time.y;
        gl_FragColor.w -= randomFrame;
      }
    }
    if(isUpdate || gl_FragColor.w != 0.){
      gl_FragColor.w = gl_FragColor.w + time.y;
      if(particleInfo.lifetime > 0.){
        gl_FragColor.w = min(particleInfo.lifetime, gl_FragColor.w);
      }
    }
  }
  `;
  private static gpuLocationShader=`
  #define PI  3.141592653589793
  #define PI2 6.283185307179586
  // x: totalTime y:deltaTime z:iterationCount
  uniform vec3 time;
  uniform mat4 matrixWorld;
  uniform mat4 matrixWorldPrev;
  uniform mat3 matrixNormal;
  uniform sampler2D emitLocation;
  highp vec3 random(const in vec3 v) {
    const highp float c = 43758.5453;
    const highp mat3 coeffs = mat3(
      165.15, 253.34, 323.22,
      241.49, 329.07, 147.79,
      376.31, 143.45, 281.63
    );
    highp vec3 sn = mod(coeffs * v, PI);
    return fract(sin(sn) * c);
  }
  highp vec3 getVortex(const in vec4 info, const in vec4 worldLocation) {
    vec3 axis = vec3(0,1,0);
    float len = distance(axis, vec3(worldLocation.x,0,worldLocation.z));
    vec3 vortex = cross(axis.xyz, normalize(worldLocation.xyz));
    vortex = mix(
      vortex,
      normalize(axis - vec3(worldLocation.x,1,worldLocation.z)),
      mix(0., info.w / particleInfo.lifetime, len - 1.)
    );
    vortex = vortex * time.y / (particleInfo.mass * (.5 + gl_FragColor.w));
    vortex *= 5. * particleInfo.forces_vortex / pow(len, 2.);
    // 阻尼
    vortex -= vortex * particleInfo.forces_damping;
    return vortex;
  }
  void main()	{
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    vec4 info = texture2D( textureInfo, uv );
    gl_FragColor = texture2D( textureLocation, uv );
    vec4 velocity = texture2D( textureVelocity, uv ); 
    if((particleInfo.lifetime>0. && info.w >= particleInfo.lifetime) || info.w <= 0.){
      vec3 randomFactor = vec3(
        info.x*345. + info.y*234. + time.x*123.,
        info.x*345. - info.y*123. - time.y*234.,
        info.y*456. + time.x*456.
      );
      vec3 location = texture2D( emitLocation, uv ).xyz + normalize(random(randomFactor) - .5) * particleInfo.emit_random;
      vec4 worldLocation = mix(matrixWorld * vec4(location,1), matrixWorldPrev * vec4(location,1), fract(randomFactor.x));
      gl_FragColor.xyz = worldLocation.xyz;
    }else{
      // 力场加成
      // 漩涡
      vec3 vortex = getVortex(info, gl_FragColor);
      // 速度加成
      vec4 worldLocation = vec4(velocity.xyz * time.y,0);
      gl_FragColor.xyz += worldLocation.xyz;
      gl_FragColor.xyz += vortex.xyz;
    }
  }
  `;
  private static gpuVelocityShader=`
  #define PI  3.141592653589793
  #define PI2 6.283185307179586
  // x: totalTime y:deltaTime z:iterationCount
  uniform vec3 time;
  uniform sampler2D emitNormal;
  uniform mat3 matrixNormal;
  highp float randomFloat(const in vec3 v) { 
    const highp float c = 43758.5453;
    return fract(sin(dot(v, vec3(12.9898, 78.233, 37.7373))) * c);
  }
  highp vec3 random(const in vec3 v) {
    const highp float c = 43758.5453;
    const highp mat3 coeffs = mat3(
      165.15, 253.34, 323.22,
      241.49, 329.07, 147.79,
      376.31, 143.45, 281.63
    );
    highp vec3 sn = mod(coeffs * v, PI);
    return fract(sin(sn) * c);
  }
  void main()	{
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    gl_FragColor = texture2D( textureVelocity, uv );
    vec4 info = texture2D( textureInfo, uv );
    vec3 randomFactor = vec3(
      info.x*345. + info.y*234. - time.x*123.,
      info.x*345. - info.y*123. + time.y*234.,
      info.y*456. + time.x*456.
    );
    if(info.w<=0. || (particleInfo.lifetime>0. && info.w >= particleInfo.lifetime)){
      gl_FragColor.xyz = vec3(0);
      if(particleInfo.emit_object){
        gl_FragColor.xyz += matrixNormal * (texture2D( emitNormal, uv ).xyz * particleInfo.velocity_normal_factor);
      }
      gl_FragColor.xyz += particleInfo.velocity_object_factor;
      gl_FragColor.xyz += normalize(random(randomFactor) - .5) * particleInfo.velocity_factor_random;
    }else{
      vec3 composite_force = vec3(0);
      // 常力
      composite_force += vec3(0,0,0) * particleInfo.forces_force;
      // 风力
      composite_force += vec3(0,0,0) * particleInfo.forces_wind;
      // 重量
      composite_force += vec3(0,-9.8,0) * particleInfo.forces_gravity;
      // 合力
      composite_force *= particleInfo.forces_all;
      // ft = mv
      vec3 velocity = composite_force * time.y / (particleInfo.mass * (.5 + gl_FragColor.w));
      gl_FragColor.xyz += velocity;
      // 紊流+布朗
      vec3 turbulence = normalize(random(gl_FragColor.xyz + randomFactor)-.5)*1000.;
      gl_FragColor.xyz += (turbulence - gl_FragColor.xyz) / 200. * time.y * (particleInfo.forces_brownian + particleInfo.forces_turbulence);
      // 阻尼
      gl_FragColor.xyz -= gl_FragColor.xyz * particleInfo.forces_damping;
    }
  }`;
  /* GPU 粒子数量 */
  private get GPUCount() {
    return this.attributes.uv.count;
  }
  /* GPU 纹理尺寸 */
  private get GPUWidth() {
    return Math.sqrt(this.GPUCount);
  }
  private gpuCompute: GPUComputationRenderer|null=null;
  private gpuInfo: Variable|null=null;
  private gpuLocation: Variable|null=null;
  private gpuVelocity: Variable|null=null;
  constructor(object: Particle) {
    super();
    this.setAttribute(
      'position',
      new BufferAttribute(new Float32Array(object.particleInfo.count! * 3), 3),
    );
    this.setAttribute(
      'uv',
      new BufferAttribute(new Float32Array(object.particleInfo.count! * 2), 2),
    );
    this.initUV();
    // this.initDefines();
  }
  private init(
    renderer: WebGLRenderer,
    scene: Scene,
    camera: Camera,
    object: Particle,
  ) {
    this.initGPU(renderer, object, camera);
  }
  public setTextureData(texture: DataTexture, x: ((index: number) => number)|number = 1, y = x, z = y, w = z) {
    const theArray = texture.image.data;
    for (let k = 0, kl = theArray.length / 4; k < kl; k += 1) {
      theArray[k * 4 + 0] = typeof x === 'function' ? x(k) : x;
      theArray[k * 4 + 1] = typeof y === 'function' ? y(k) : y;
      theArray[k * 4 + 2] = typeof z === 'function' ? z(k) : z;
      theArray[k * 4 + 3] = typeof w === 'function' ? w(k) : w;
    }
    texture.wrapS = ClampToEdgeWrapping;
    texture.wrapT = ClampToEdgeWrapping;
    texture.magFilter = NearestFilter;
    texture.minFilter = NearestFilter;
  }
  private initEmitData(locationTexture: DataTexture, normalTexture: DataTexture, object?: Mesh|Line|Points) {
    if (object?.geometry.attributes?.position) {
      const position = object?.geometry.attributes?.position;
      const locationArray = locationTexture.image.data;
      for (let k = 0, kl = locationArray.length / 4; k < kl; k += 1) {
        const index = Math.floor(Math.random() * position.count);
        locationArray[k * 4 + 0] = position.array[position.itemSize * index];
        if (position.itemSize > 1) locationArray[k * 4 + 1] = position.array[position.itemSize * index + 1];
        if (position.itemSize > 2) locationArray[k * 4 + 2] = position.array[position.itemSize * index + 2];
        if (object?.geometry.attributes?.normal) {
          const normal = object?.geometry.attributes?.normal;
          const normalArray = normalTexture.image.data;
          normalArray[k * 4 + 0] = normal.array[normal.itemSize * index];
          if (normal.itemSize > 1) normalArray[k * 4 + 1] = normal.array[normal.itemSize * index + 1];
          if (normal.itemSize > 2) normalArray[k * 4 + 2] = normal.array[normal.itemSize * index + 2];
        }
      }
    }
    locationTexture.wrapS = ClampToEdgeWrapping;
    locationTexture.wrapT = ClampToEdgeWrapping;
    locationTexture.magFilter = NearestFilter;
    locationTexture.minFilter = NearestFilter;

    normalTexture.wrapS = ClampToEdgeWrapping;
    normalTexture.wrapT = ClampToEdgeWrapping;
    normalTexture.magFilter = NearestFilter;
    normalTexture.minFilter = NearestFilter;
  }
  private initInfoData(texture: DataTexture, object: Particle) {
    this.setTextureData(texture, index => index, () => Math.random(), 0, () => -1.5 * Math.random()
    * object.particleInfo.lifetime_random!
    * object.particleInfo.lifetime!);
  }
  private initLocationData(texture: DataTexture, object: Particle, emitLocationTexture?: DataTexture) {
    enum weight {
      x, y, z
    };
    const location = (i: 'x'|'y'|'z') => (index: number) => emitLocationTexture!.image.data[index * 4 + weight[i]];
    this.setTextureData(
      texture, location('x'), location('y'), location('z'),
      () => (1 - Math.random() * object.particleInfo.size_random!),
    );
  }
  private initVelocityData(
    texture: DataTexture,
    object: Particle,
    locationTexture?: DataTexture,
    emitNormalTexture?: DataTexture,
  ) {
    enum weight {
      x, y, z
    };
    const velocity = (i: 'x'|'y'|'z') => (index: number) => {
      let vel = 0;
      if (object.particleInfo.emit_object) {
        vel += emitNormalTexture!.image.data[index * 4 + weight[i]];
      }
      vel += object.particleInfo.velocity?.object_factor?.[i] || 0;
      vel += (Math.random() - .5) * 2 * (object.particleInfo.velocity?.factor_random || 0);
      return vel;
    };
    this.setTextureData(texture, velocity('x'), velocity('y'), velocity('z'), index => (locationTexture ? locationTexture.image.data[4 * index + 3] : 1));
  }
  private initGPU(renderer: WebGLRenderer, object: Particle, camera: Camera) {
    this.gpuCompute = new GPUComputationRenderer(this.GPUWidth, this.GPUWidth, renderer);
    if (renderer.capabilities.isWebGL2 === false) {
      this.gpuCompute.setDataType(HalfFloatType);
    }
    const dtEmitLocation = this.gpuCompute.createTexture();
    const dtEmitNormal = this.gpuCompute.createTexture();
    this.initEmitData(dtEmitLocation, dtEmitNormal, object.particleInfo.emit_object);
    // index random age lifetime
    const dtInfo = this.gpuCompute.createTexture();
    this.initInfoData(dtInfo, object);
    // location size
    const dtLocation = this.gpuCompute.createTexture();
    this.initLocationData(dtLocation, object, dtEmitLocation);
    // velocity
    const dtVelocity = this.gpuCompute.createTexture();
    this.initVelocityData(dtVelocity, object, dtLocation, dtEmitNormal);
    const gpuInfo = this.gpuCompute.addVariable(
      'textureInfo',
      `${object.particleInfoStruct}\n${ParticleGeometry.gpuInfoShader}`, dtInfo,
    );
    this.gpuInfo = gpuInfo;
    this.gpuInfo.material.customProgramCacheKey = () => 'ParticleGeometry:Info';
    this.gpuLocation = this.gpuCompute.addVariable(
      'textureLocation',
      `${object.particleInfoStruct}\n${ParticleGeometry.gpuLocationShader}`, dtLocation,
    );
    this.gpuLocation.material.customProgramCacheKey = () => 'ParticleGeometry:Location';
    this.gpuVelocity = this.gpuCompute.addVariable(
      'textureVelocity',
      `${object.particleInfoStruct}\n${ParticleGeometry.gpuVelocityShader}`, dtVelocity,
    );
    this.gpuVelocity.material.customProgramCacheKey = () => 'ParticleGeometry:Velocity';

    this.gpuInfo.material.uniforms.emitLocation = new Uniform(dtEmitLocation);
    this.gpuLocation.material.uniforms.emitLocation = this.gpuInfo.material.uniforms.emitLocation;
    this.gpuVelocity.material.uniforms.emitLocation = this.gpuInfo.material.uniforms.emitLocation;

    this.gpuInfo.material.uniforms.emitNormal = new Uniform(dtEmitNormal);
    this.gpuLocation.material.uniforms.emitNormal = this.gpuInfo.material.uniforms.emitNormal;
    this.gpuVelocity.material.uniforms.emitNormal = this.gpuInfo.material.uniforms.emitNormal;

    this.gpuInfo.material.uniforms.particleInfo = object.particleInfoUniform;
    this.gpuLocation.material.uniforms.particleInfo = this.gpuInfo.material.uniforms.particleInfo;
    this.gpuVelocity.material.uniforms.particleInfo = this.gpuInfo.material.uniforms.particleInfo;
    const timeVector3 = new Vector3(0, 0, object.particleInfo.lifetime_count);
    this.gpuInfo.material.uniforms.time = new Uniform(timeVector3);
    this.gpuLocation.material.uniforms.time = this.gpuInfo.material.uniforms.time;
    this.gpuVelocity.material.uniforms.time = this.gpuInfo.material.uniforms.time;
    object.particleInfo = new Proxy(object.particleInfo, {
      set(...arg) {
        if ((arg[1] === 'lifetime_count')) {
          if (arg[0].lifetime_count !== arg[2]) {
            timeVector3.x = 0;
            timeVector3.z = arg[2] as number;
          }
        }
        return Reflect.set(...arg);
      },
    });

    this.gpuInfo.material.uniforms.matrixWorld = new Uniform(object.particleInfo.emit_object?.matrixWorld
      || object.matrixWorld);
    this.gpuLocation.material.uniforms.matrixWorld = this.gpuInfo.material.uniforms.matrixWorld;
    this.gpuVelocity.material.uniforms.matrixWorld = this.gpuInfo.material.uniforms.matrixWorld;

    const cloneMatrix = this.gpuInfo.material.uniforms.matrixWorld.value.clone();
    this.gpuInfo.material.uniforms.matrixWorldPrev = new Uniform(cloneMatrix);
    this.gpuLocation.material.uniforms.matrixWorldPrev = this.gpuInfo.material.uniforms.matrixWorldPrev;
    this.gpuVelocity.material.uniforms.matrixWorldPrev = this.gpuInfo.material.uniforms.matrixWorldPrev;

    this.gpuInfo.material.uniforms.matrixNormal = new Uniform(object.particleInfo.emit_object?.normalMatrix
      || new Matrix3());
    this.gpuLocation.material.uniforms.matrixNormal = this.gpuInfo.material.uniforms.matrixNormal;
    this.gpuVelocity.material.uniforms.matrixNormal = this.gpuInfo.material.uniforms.matrixNormal;

    this.gpuInfo.material.uniforms.viewMatrix = new Uniform(camera.matrixWorldInverse);
    this.gpuLocation.material.uniforms.viewMatrix = this.gpuInfo.material.uniforms.viewMatrix;
    this.gpuVelocity.material.uniforms.viewMatrix = this.gpuInfo.material.uniforms.viewMatrix;

    this.gpuCompute.setVariableDependencies(this.gpuInfo, [this.gpuInfo]);
    this.gpuCompute.setVariableDependencies(this.gpuLocation, [this.gpuLocation, this.gpuVelocity, this.gpuInfo]);
    this.gpuCompute.setVariableDependencies(this.gpuVelocity, [this.gpuInfo, this.gpuVelocity]);
    this.gpuCompute.init();
  }
  private initUV() {
    const uvArray = this.attributes.uv.array as Float32Array;
    const count = this.GPUCount;
    const width = this.GPUWidth;
    for (let index = 0; index < count; index++) {
      const x = (index % width) / width;
      const y = (index / width) / width;
      uvArray[index * 2] = x;
      uvArray[index * 2 + 1] = y;
    }
  }
  public get locationTexture() {
    if (this.gpuCompute) {
      return this.gpuCompute.getCurrentRenderTarget(this.gpuLocation!).texture;
    }
    return null;
  }
  public get velocityTexture() {
    if (this.gpuCompute) {
      return this.gpuCompute.getCurrentRenderTarget(this.gpuVelocity!).texture;
    }
    return null;
  }
  public get infoTexture() {
    if (this.gpuCompute) {
      return this.gpuCompute.getCurrentRenderTarget(this.gpuInfo!).texture;
    }
    return null;
  }
  private performanceTime = 0;
  private get time() {
    if (this.gpuLocation) {
      return (this.gpuLocation.material.uniforms.time.value as Vector3).x;
    }
    return 0;
  };
  private set time(t: number) {
    if (this.gpuLocation) {
      (this.gpuLocation.material.uniforms.time.value as Vector3).x = t;
    }
  };
  private get deltaTime() {
    if (this.gpuLocation) {
      return (this.gpuLocation.material.uniforms.time.value as Vector3).y;
    }
    return 0;
  };
  private set deltaTime(t: number) {
    if (this.gpuLocation) {
      (this.gpuLocation.material.uniforms.time.value as Vector3).y = Math.min(1 / 20, t);
    }
  };
  public render(
    renderer: WebGLRenderer,
    scene: Scene,
    camera: Camera,
    object: Particle,
  ) {
    if (!this.gpuCompute) {
      this.performanceTime = performance.now();
      this.init(renderer, scene, camera, object);
    } else {
      this.gpuCompute.compute();
      const currentTime = performance.now();
      this.deltaTime = (currentTime - this.performanceTime) / 1000;
      this.performanceTime = currentTime;
      this.time += this.deltaTime;
      // console.log(this.gpuInfo!.material.uniforms.modelMatrix);
      object.material.locationTexture = this.locationTexture!;
      object.material.infoTexture = this.infoTexture!;
      object.material.velocityTexture = this.velocityTexture!;
      this.gpuInfo!.material.uniforms.matrixWorldPrev.value.copy(this.gpuInfo!.material.uniforms.matrixWorld.value);
    }
  }
}
class ParticleMaterial extends ShaderMaterial {
  private static particleVertex=`
uniform float size;
uniform float scale;
uniform sampler2D locationTexture;
uniform sampler2D infoTexture;
uniform sampler2D velocityTexture;
varying mat4 particleData;
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>

void main() {
	#include <color_vertex>
	#include <begin_vertex>
  vec4 location = texture2D( locationTexture, uv.xy );
  vec4 velocity = texture2D( velocityTexture, uv.xy );
  vec4 info = texture2D( infoTexture, uv.xy );
  // index
  particleData[0][0] = info.x;
  // random
  particleData[0][1] = info.y;
  // age
  particleData[0][2] = info.z;
  // lifetime
  particleData[0][3] = info.w;
  // location + size
  particleData[1] = vec4(location.xyz, max(0., size* location.w));
  // velocity + lifetimeProgress
  particleData[2] = vec4(velocity.xyz, .000001);
  if(particleInfo.lifetime>0.) {
    particleData[2][3] = particleData[0][3] / particleInfo.lifetime;
  }
  
  transformed = particleData[1].xyz;
	#include <morphtarget_vertex>
	// #include <project_vertex>
  vec4 mvPosition = vec4( transformed, 1.0 );
  #ifdef USE_INSTANCING
    mvPosition = instanceMatrix * mvPosition;
  #endif
  
  mvPosition = viewMatrix * mvPosition;
  gl_Position = projectionMatrix * mvPosition;

  // 由大到小
  // gl_PointSize *= (cos(particleData[2][3] * PI)+1.)/2.;
	#ifdef USE_SIZEATTENUATION

		bool isPerspective = isPerspectiveMatrix( projectionMatrix );

		if ( isPerspective ) particleData.size *= ( scale / - mvPosition.z );

	#endif
  gl_PointSize = particleData[1].w;

	#include <logdepthbuf_vertex>
	#include <clipping_planes_vertex>
	#include <worldpos_vertex>
	#include <fog_vertex>
}
`;
  private static particleFragment=`
uniform vec3 diffuse;
uniform float opacity;
// index              particleData[0][0]
// random             particleData[0][1]
// age                particleData[0][2]
// lifetime           particleData[0][3]
// lifetimeProgress   particleData[2][3]
// location           particleData[1].xyz
// size               particleData[1].w
// velocity           particleData[2].xyz
varying mat4 particleData;

#include <common>

highp vec3 random(const in vec3 v) {
  const highp float c = 43758.5453;
  const highp mat3 coeffs = mat3(
    165.15, 253.34, 323.22,
    241.49, 329.07, 147.79,
    376.31, 143.45, 281.63
  );
  highp vec3 sn = mod(coeffs * v, PI);
  return fract(sin(sn) * c);
}
#include <color_pars_fragment>
#include <map_particle_pars_fragment>
#include <alphatest_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>

void main() {
  if(particleData[2][3] <= 0. || particleData[2][3] >= 1.){
    discard;
  }
  if(distance(gl_PointCoord,vec2(.5))>.5*(1.-particleData[2][3])){
    discard;
  }
	#include <clipping_planes_fragment>

	vec3 outgoingLight = vec3( 0.0 );
	vec4 diffuseColor = vec4( diffuse, opacity );

	#include <logdepthbuf_fragment>
	#include <map_particle_fragment>
	#include <color_fragment>
	#include <alphatest_fragment>
  vec3 randomColor = random(vec3((particleData[0][1]+10.)*123.,(particleData[0][1]+20.)*234.,(particleData[0][1]+30.)*345.));
	outgoingLight = diffuseColor.rgb;
	// outgoingLight = mix(randomColor, vec3(.5,0,1), particleData[2][3] * 1.5);
	outgoingLight = mix(vec3(0.075,1,1), randomColor, particleData[2][3] * 2.);
	// outgoingLight = randomColor;
  // outgoingLight.xyz *= 1. - distance(gl_PointCoord,vec2(.5))*2.;
  // outgoingLight.xyz *= (1. - particleData[2][3] );
	#include <output_fragment>
	#include <tonemapping_fragment>
	#include <encodings_fragment>
	#include <fog_fragment>
	#include <premultiplied_alpha_fragment>

}
  `;
  public isPointsMaterial = true;
  public size!: number;
  public color = new Color('#ff0');
  public map = null;
  public alphaMap = null;
  public sizeAttenuation = false;
  public transparent = true;
  public blending = CustomBlending;
  public blendEquation = MaxEquation;
  public depthWrite = false;
  public get locationTexture(): Texture {
    return this.uniforms.locationTexture.value;
  }
  public set locationTexture(val: Texture) {
    this.uniforms.locationTexture.value = val;
  }
  public get velocityTexture(): Texture {
    return this.uniforms.velocityTexture.value;
  }
  public set velocityTexture(val: Texture) {
    this.uniforms.velocityTexture.value = val;
  }
  public get infoTexture(): Texture {
    return this.uniforms.infoTexture.value;
  }
  public set infoTexture(val: Texture) {
    this.uniforms.infoTexture.value = val;
  }
  public get scale(): number {
    return this.uniforms.scale.value;
  }
  public set scale(val: number) {
    this.uniforms.scale.value = val;
  }
  public customProgramCacheKey() {
    return 'ParticleMaterial';
  }
  constructor(object: Particle) {
    super({
      uniforms: UniformsUtils.merge([
        UniformsLib.points,
        UniformsLib.fog,
        {
          locationTexture: { value: null },
          velocityTexture: { value: null },
          infoTexture: { value: null },
          particleInfo: object.particleInfoUniform,
        },
      ]),
      vertexShader: `${object.particleInfoStruct}\n${ParticleMaterial.particleVertex}`,
      fragmentShader: `${object.particleInfoStruct}\n${ParticleMaterial.particleFragment}`,
    });
    Object.defineProperty(this, 'size', {
      get() {
        return object.particleInfo.size;
      },
      set(size: number) {
        object.particleInfo.size = Math.max(1, size);
      },
      enumerable: true,
    });
  }
  public render(
    renderer: WebGLRenderer,
    scene: Scene,
    camera: Camera,
    object: Particle,
  ) {

  }
}
/* Particle mesh */
export default class Particle extends Points {
  matrixWorld: any;
  private static mixSetting(params: ParticleParams) {
    const setting: ParticleParams = {
      count: 16,
      lifetime: 1,
      lifetime_count: -1,
      lifetime_random: 0,
      mass: 1,
      size: 1,
      size_random: 0,
      emit_object: undefined,
      emit_random: 0,
      instance_object: undefined,
      velocity: {
        normal_factor: 1,
        tangent_factor: 0,
        object_factor: new Vector3(),
        factor_random: 0,
      },
      rotation: {
        factor_random: 0,
        velocity_factor: 0,
      },
      forces: {
        brownian: 0,
        damping: 0,
        all: 1,
        force: 1,
        wind: 1,
        gravity: 1,
        vortex: 1,
        texture: 1,
        turbulence: 0,
      },
    };
    const mapObject = (obj: any, obj2: any, parentKey = '') => {
      Object.keys(obj2).forEach((key) => {
        const type = typeof obj[key];
        const type2 = typeof obj2[key];
        if (type === 'object' && type2 === 'object' && !(
          (obj2[key].isObject3D || obj2[key].isColor
            || obj2[key].isMatrix3 || obj2[key].isMatrix4
            || obj2[key].isVector2 || obj2[key].isVector3 || obj2[key].isVector4
            || obj2[key].isTexture || obj2[key].isQuaternion)
        )) {
          mapObject(obj[key], obj2[key], key);
        } else {
          if (key.includes('_random') && parentKey !== 'velocity' && key !== 'emit_random') {
            obj2[key] = Math.max(0, Math.min(obj2[key] ?? 0, 1));
          }
          obj[key] = obj2[key] ?? obj[key];
        }
      });
    };
    mapObject(setting, params);
    setting.count = Math.max(1, Math.min(1024, Math.round(Math.sqrt(setting.count || 4)))) ** 2;
    return setting;
  }
  declare public geometry: ParticleGeometry;
  declare public material: ParticleMaterial;
  public particleInfo!: ParticleParams;
  #particleInfoUniform!: Uniform;
  public get particleInfoUniform() {
    if (this.#particleInfoUniform) return this.#particleInfoUniform;
    this.#particleInfoUniform = new Uniform({});
    const mapObject = (obj: any, prefix = '') => {
      Object.keys(obj).forEach((key) => {
        const currentkey =  `${prefix ? (`${prefix}_`) : ''}${key}`;
        const value = obj[key];
        const type = typeof value;
        if (type === 'undefined' || value === null || (type === 'object' && value.isObject3D)) {
          Object.defineProperty(this.#particleInfoUniform?.value, currentkey, {
            get() {
              return !!obj[key];
            },
            enumerable: true,
          });
        } else if (
          type === 'number' || type === 'boolean'
          || (type === 'object' && (value.isColor
          || value.isMatrix3 || value.isMatrix4
          || value.isVector2 || value.isVector3 || value.isVector4
          || value.isTexture || value.isQuaternion))) {
          Object.defineProperty(this.#particleInfoUniform?.value, currentkey, {
            get() {
              return obj[key];
            },
            enumerable: true,
          });
        } else if (type === 'object') {
          mapObject(value, currentkey);
        }
      });
    };
    mapObject(this.particleInfo);
    return this.#particleInfoUniform;
  }
  public get particleInfoStruct() {
    const dependStruct: string[] = [];
    const mapStruct = (obj: any, span = '') => {
      const currentSpan = `  ${span}`;
      let structStr = '';
      Object.keys(obj).forEach((key) => {
        switch (typeof obj[key]) {
          case 'boolean': {
            structStr += `${currentSpan}bool ${key};\n`;
            break;
          }
          case 'number': {
            structStr += `${currentSpan}float ${key};\n`;
            break;
          }
          default: {
            if (obj[key] instanceof Vector2) {
              structStr += `${currentSpan}vec2 ${key};\n`;
            } else if (obj[key] instanceof Vector3) {
              structStr += `${currentSpan}vec3 ${key};\n`;
            } else if (obj[key] instanceof Vector4) {
              structStr += `${currentSpan}vec4 ${key};\n`;
            } else if (obj[key] instanceof Texture) {
              structStr += `${currentSpan}sampler2D ${key};\n`;
            } else {
              ;
              const structName = `Particle${key.split('_')
                .map(str => str.replace(/^[a-z]/, a => a.toUpperCase()))
                .join('')}`;
              dependStruct.push(`${currentSpan}struct ${structName} {
${mapStruct(obj[key], currentSpan)}};`);
              structStr += `${currentSpan}${structName} ${key};\n`;
            }
          }
        }
      });
      return structStr;
    };
    const infoStruct = `struct ParticleInfo {
${mapStruct(this.particleInfoUniform.value)}};`;
    return `${dependStruct.join('\n')}
${infoStruct} 
uniform ParticleInfo particleInfo;`;
  };
  public run(lifeCount: number) {
    if (typeof lifeCount === 'number') {
      this.particleInfo.lifetime_count = Math.max(-1, lifeCount);
    } else {
      throw new Error('参数错误');
    }
  }
  constructor(params: ParticleParams) {
    super();
    this.particleInfo = Particle.mixSetting(params);
    this.geometry = new ParticleGeometry(this);
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const _this = this;
    let obj: any = _this;
    if (true || !this.particleInfo.instance_object) {
      this.material = new ParticleMaterial(this);
    } else if (
      (this.particleInfo.instance_object as Mesh).isMesh
      || (this.particleInfo.instance_object as Points).isPoints
      || (this.particleInfo.instance_object as Line).isLine
    ) {
      obj = this.particleInfo.instance_object;
      this.visible = true;
    }
    this.castShadow = false;
    this.frustumCulled = false;
    this.receiveShadow = false;
    obj.onBeforeRender = new Proxy(obj.onBeforeRender, {
      apply(target, thisArg, argumentsList: [
        renderer: WebGLRenderer,
        scene: Scene,
        camera: Camera,
        geometry: ParticleGeometry,
        material: ParticleMaterial,
        group: Group,
      ]) {
        _this.geometry.render(
          argumentsList[0],
          argumentsList[1],
          argumentsList[2],
          _this,
        );
        // @ts-ignore
        // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
        if (argumentsList[4]?.render) {
          argumentsList[4].render(
            argumentsList[0],
            argumentsList[1],
            argumentsList[2],
            _this,
          );
        }
        target.apply(thisArg, argumentsList);
      },
    });
  }
}
